
package com.studiofortress.sf.audio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;

/**
 * An implementation of the AudioPlayer interface designed for playing midi
 * files.
 * 
 * @author Joseph Lenton
 */
/*
 * Note that the volume management code is based on this:
 * http://forums.sun.com/thread.jspa?threadID=5237086
 */
public class MidiPlayer implements AudioPlayer
{
    private static final int CHANGE_VOLUME = 7;
    
    private URL url;
    private int volume;
    private Sequencer sequencer;
    private boolean stopped;
    Synthesizer synthesizer;

    public MidiPlayer(String path)
    {
        stopped = true;
        volume = 100;
        synthesizer = null;
        
        try {
            if (path == null) {
                throw new IllegalArgumentException("The given path cannot be null.");
            }

            url = ClassLoader.getSystemClassLoader().getResource(path);

            File inputFile = null;
            if (url == null) {
                inputFile = new File(path);
                if (!inputFile.exists()) {
                    throw new IllegalArgumentException("The given path was not found: " + path);
                }
            } else {
                inputFile = new File(url.toURI());
            }

            initialize(new FileInputStream(inputFile));
        } catch (URISyntaxException e) {
            throw new RuntimeException("Failed to load midi track: " + path, e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Failed to load midi track: " + path, e);
        }
    }

    public MidiPlayer(InputStream input)
    {
        initialize(input);
    }

    private final void initialize(InputStream input)
    {
        if (input == null) {
            throw new IllegalArgumentException("The given input cannot be null.");
        }

        try {
            Sequence sequence = MidiSystem.getSequence(input);

            // Create a sequencer for the sequence
            sequencer = MidiSystem.getSequencer();
            sequencer.setLoopEndPoint(-1); // loop at the end of the midi
            synthesizer = MidiSystem.getSynthesizer();
            sequencer.open();
            synthesizer.open();
            sequencer.setSequence(sequence);

            if (synthesizer.getDefaultSoundbank() == null) {
                // then you know that java sound is using the hardware soundbank
                sequencer.getTransmitter().setReceiver( MidiSystem.getReceiver() );
                // in the case of windows, the above MidiSystem.getReceiver() call returns
            } else {
                sequencer.getTransmitter().setReceiver( synthesizer.getReceiver() );
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to load midi track", e);
        } catch (MidiUnavailableException e) {
            throw new RuntimeException("Failed to load midi track", e);
        } catch (InvalidMidiDataException e) {
            throw new RuntimeException("Failed to load midi track", e);
        }

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run()
            {
                if (sequencer.isOpen()) {
                    sequencer.close();
                }
            }
        });
    }

    /**
     * Closes the sequencer this holds.
     * @throws java.lang.Throwable
     */
    @Override
    protected void finalize() throws Throwable
    {
        super.finalize();
        if (sequencer.isOpen()) {
            sequencer.close();
        }
    }

    @Override
    public void play()
    {
        sequencer.start();
        stopped = false;
    }

    public boolean isPlaying()
    {
        return sequencer.isRunning();
    }

    @Override
    public void stop()
    {
        if (sequencer.isOpen()) {
            sequencer.stop();
            sequencer.setMicrosecondPosition(0); // reset to the beginning
            stopped = true;
        }
    }

    @Override
    public boolean isStopped()
    {
        return stopped;
    }

    @Override
    public void pause()
    {
        if (sequencer.isOpen()) {
            sequencer.stop();
        }
    }

    public boolean isPaused()
    {
        return !isStopped() && !isPlaying();
    }

    @Override
    public void setRepeat(boolean repeat)
    {
        if (repeat) {
            sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
        } else {
            sequencer.setLoopCount(0);
        }
    }

    /**
     * The new volume for the midi player.
     *
     * @param volume A value from 0 to 100. Anything outside this will throw an exception.
     */
    public void setVolume(int volume)
    {
        if (volume < 0 || volume > 100) {
            throw new IllegalArgumentException("The given volume must be a value from 0 to 100, inclusive.");
        }
        
        this.volume = volume;
        // Java uses 0 to 127, not 0 to 100, so we convert!
        volume = (int)Math.round(((double)volume / 100.0) * 127.0);

        try {
            if (synthesizer != null) {
                if(synthesizer.getDefaultSoundbank() == null) {
                    // HARDWARE SYNTHESIZER
                    ShortMessage volumeMessage = new ShortMessage();
                    for( int i = 0; i < 16; i++ ) {
                        volumeMessage.setMessage(ShortMessage.CONTROL_CHANGE,
                                i, CHANGE_VOLUME, volume);
                        MidiSystem.getReceiver().send( volumeMessage, -1 );
                    }
                } else {
                    MidiChannel[] channels = synthesizer.getChannels();

                    // gain is a value between 0 and 1 (loudest)
                    for (int i = 0; i < channels.length; i++) {
                        channels[i].controlChange(7, volume);
                    }
                }
            }
        } catch (InvalidMidiDataException e) {
            e.printStackTrace();
        } catch (MidiUnavailableException e) {
            e.printStackTrace();
        }
    }

    /**
     * @return The current level of the volume.
     */
    public int getVolume()
    {
        return volume;
    }

    @Override
    public boolean isRepeating()
    {
        return sequencer.getLoopCount() == sequencer.LOOP_CONTINUOUSLY;
    }
}
